<?php

/**
 * This file is part of the lisifu/tiktok.
 */

namespace EasyTiktok\MiniProgram\Payment;

use EasyTiktok\Kernel\BaseClient;
use EasyTiktok\Kernel\Encryptor;
use EasyTiktok\Kernel\Exceptions\HttpException;
use EasyTiktok\Kernel\Exceptions\InvalidConfigException;
use EasyTiktok\Kernel\Http\StreamResponse;
use EasyTiktok\Kernel\ServiceContainer;
use EasyTiktok\Kernel\Support\Collection;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Http\Message\ResponseInterface;

/**
 *
 */
class Client extends BaseClient
{
    protected $encryptor;

    public function __construct(ServiceContainer $app, $accessToken = null)
    {
        parent::__construct($app, $accessToken);
        if ($app['config']['sandbox']) {
            $app->config->set('http.base_uri', 'https://open-sandbox.douyin.com/api/');
        }
        $this->needAccessToken = false;
    }

    /**
     * 预下单接口
     * @param string $out_order_no 开发者侧的订单号
     * @param int $total_amount 支付价格。 单位为[分]
     * @param string $subject 商品描述。 长度限制不超过 128 字节且不超过 42 字符
     * @param string $body 商品详情 长度限制不超过 128 字节且不超过 42 字符
     * @param string $notify_url 商户自定义回调地址，必须以 https 开头，支持 443 端口。 指定时，支付成功后抖音会请求该地址通知开发者
     * @param int $valid_time 订单过期时间(秒)。最小5分钟，最大2天
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     * @author lisifu <lisifu@139.com>
     */
    public function create(string $out_order_no, int $total_amount, string $subject, string $body, string $notify_url, int $valid_time = 30 * 60, array $optional = [])
    {

        $params = array_merge([
            'out_order_no' => $out_order_no,
            'total_amount' => $total_amount,
            'subject' => $subject,
            'body' => $body,
            'valid_time' => $valid_time,
            'notify_url' => $notify_url
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/create_order', $params);
    }

    /**
     * 支付结果查询
     * @param string $out_order_no 开发者侧的订单号
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function queryOrder(string $out_order_no, array $optional = [])
    {
        $params = array_merge([
            'out_order_no' => $out_order_no,
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/query_order', $params);
    }

    /**
     * 发起退款
     * @param string $out_order_no 商户订单号
     * @param string $refund_no 商户退款号
     * @param int $refund_fee 退款金额，单位分
     * @param string $notify_url 回调地址,必须以 https 开头，支持 443 端口
     * @param string $reason 退款原因
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function refund(
        $out_order_no,
        $refund_no,
        int $refund_fee,
        $notify_url,
        $reason = '要求退款', array $optional = [])
    {
        $params = array_merge([
            'out_order_no' => $out_order_no,
            'out_refund_no' => $refund_no,
            'reason' => $reason,
            'refund_amount' => $refund_fee,
            'notify_url' => $notify_url
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/create_refund', $params);
    }


    /**
     * 退款结果查询
     * @param string $out_refund_no 开发者侧的退款单号
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function queryRefund(string $out_refund_no, array $optional = [])
    {
        $params = array_merge([
            'out_order_no' => $out_order_no,
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/query_refund', $params);
    }

    /**
     * 发起结算及分账
     * - https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/settlements/settlement
     * @param string $out_settle_no 开发者侧的结算单号
     * @param string $out_order_no 商户订单单号
     * @param array $settle_params 仅支持传入第三方分账方（非商户和平台）,包含{merchant_uid,amount}
     * @param string $settle_desc 结算描述，长度限制 80 个字符
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function settle(string $out_settle_no, string $out_order_no, array $settle_params, string $settle_desc = '主动结算', array $optional = [])
    {
        $params = array_merge([
            'out_settle_no' => $out_settle_no,
            'out_order_no' => $out_order_no,
            'settle_desc' => $settle_desc,
            'settle_params' => json_encode($settle_params, JSON_UNESCAPED_UNICODE)
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/settle', $params);
    }

    /**
     * 查询 结算及分账结果
     * @param string $out_settle_no
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function querySettle(string $out_settle_no, array $optional = [])
    {
        $params = array_merge([
            'out_settle_no' => $out_settle_no,
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/query_settle', $params);
    }

    /**
     * 查询 订单可分账余额
     * @param string $out_order_no 商户侧支付单号
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function queryUnsettleAmount(string $out_order_no, array $optional = [])
    {
        $params = array_merge([
            'out_settle_no' => $out_settle_no,
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/unsettle_amount', $params);
    }

    /**
     * 发起退分账
     * - 支付订单已经分账的情况下发生退款，可以调用此接口将已分账的资金从分账接收方的账户回退给结算发起方的现金户，再发起退款
     * @param string $out_return_no 回退单号
     * @param string $out_settle_no 商户侧的结算号
     * @param string $return_desc 回退描述，长度限制 100 个字符
     * @param string $merchant_uid 回退商户号，即原分账方商户号
     * @param int $return_amount 回退金额，单位分
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function createReturn(string $out_return_no, string $out_settle_no, string $return_desc, string $merchant_uid, int $return_amount, array $optional = [])
    {
        $params = array_merge([
            'out_settle_no' => $out_settle_no,
            'out_return_no' => $out_return_no,
            'return_desc' => $return_desc,
            'merchant_uid' => $merchant_uid,
            'return_amount' => $return_amount,
        ], $optional);
        return $this->toRequest('apps/ecpay/v1/create_return', $params);
    }

    /**
     * 查询 商户余额
     * @param string $merchant_uid 商户号
     * @param string $channel_type 提现渠道枚举值: alipay: 支付宝、wx: 微信、hz: 抖音支付、yeepay: 易宝
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function queryBalance(string $merchant_uid, string $channel_type, array $optional = [])
    {
        $params = array_merge([
            'merchant_uid' => $merchant_uid,
            'channel_type' => $channel_type,
        ], $optional);
        return $this->toRequest('apps/ecpay/saas/query_merchant_balance', $params);
    }

    /**
     * 商户提现
     * @param string $out_order_id 提现单号
     * @param string $merchant_uid 商户号
     * @param string $channel_type 提现渠道枚举值: alipay: 支付宝、wx: 微信、hz: 抖音支付、yeepay: 易宝
     * @param int $withdraw_amount 提现金额；单位分
     * @param string $callback 提现结果通知接口
     * @param array $optional 其它非必填项参数
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function withdraw(string $out_order_id, string $merchant_uid, string $channel_type, int $withdraw_amount, string $callback, array $optional = [])
    {
        $params = array_merge([
            'merchant_uid' => $merchant_uid,
            'channel_type' => $channel_type,
            'withdraw_amount' => $withdraw_amount,
            'out_order_id' => $out_order_id,
            'callback' => $callback,
        ], $optional);
        return $this->toRequest('apps/ecpay/saas/merchant_withdraw', $params);
    }

    /**
     * 订单同步
     * - 接入担保支付、但未接入交易系统的订单，开发者必须通过订单同步接口将订单信息推送到抖音订单中心，便于用户在抖音订单中心查找订单信息，并可再次回访小程序
     * @param string $open_id 小程序用户的 open_id
     * @param array $order_detail 根据不同订单类型有不同的结构体，<a>https://developer.open-douyin.com/docs/resource/zh-CN/mini-app/develop/server/ecpay/order/order-sync#_%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0</a>
     * @param int $order_status 普通小程序订单订单状态，POI 订单可以忽略
     *  - 0：待支付、1：已支付、2：已取消、4：已核销（核销状态是整单核销,即一笔订单买了 3 个券，核销是指 3 个券核销的整单）、5：退款中、6：已退款、8：退款失败；注意：普通小程序订单必传
     * @param int $update_time 订单信息变更时间，13 位毫秒级时间戳
     * @param int $order_type 订单类型，枚举值: 0：普通小程序订单（非POI订单）、9101：团购券订单（POI 订单）、9001：景区门票订单（POI订单)
     * @return array|Collection|object|ResponseInterface|string
     * @throws GuzzleException
     * @throws HttpException
     * @throws InvalidConfigException
     */
    public function pushOrder(string $open_id, array $order_detail, int $order_status, int $update_time, int $order_type = 0)
    {
        $this->needAccessToken = true;
        $params = [
            'app_name' => 'douyin',
            'open_id' => $open_id,
            'order_detail' => json_encode($order_detail, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
            'order_status' => $order_status,
            'order_type' => $order_type,
            'update_time' => $update_time
        ];
        return $this->toRequest('apps/order/v2/push', $params, false);
    }

    /**
     * 发起请求
     *
     * @param string $url
     * @param array $params
     * @param bool $needSign 是否需要 签名【sign】字段
     *
     * @return array|Collection|object|ResponseInterface|string
     *
     * @throws InvalidConfigException
     * @throws GuzzleException|HttpException
     */
    public function toRequest($url, array $params, $needSign = true)
    {
        if ($needSign) {
            $params['app_id'] = $this->app['config']['app_id'];
            $params['sign'] = $this->signature($params);
        }
        return $this->httpPostJson($url, $params);
    }

    /**
     * 支付回调通知处理
     * @param \Closure $payClosure 用于处理的方法函数,返回 true：表示处理成功，其它表示错误信息
     * @return array
     */
    public function handleNotify(\Closure $payClosure)
    {
        $message = file_get_contents('php://input');
        $message = json_decode($message, true);
        if (!$this->checkCallbackSignature($message)) {
            return [
                'err_no' => 100,
                'err_tips' => '验签失败'
            ];
        }
        $type = $message['type'];
        $message = json_decode($message['msg'], true);
        $message['type'] = $type;
        $info = call_user_func($payClosure, $message);
        if (true === $info) {
            return json_encode([
                'err_no' => 0,
                'err_tips' => 'success'
            ]);
        }

        return json_encode([
            'err_no' => 100,
            'err_tips' => $info
        ], JSON_UNESCAPED_UNICODE);
    }

    /**
     * 生成签名
     * @param $map
     * @return string
     */
    private function signature($map)
    {
        $rList = array();
        foreach ($map as $k => $v) {
            if ($k == "app_id" || $k == "sign" || $k == "thirdparty_id")
                continue;
            $value = trim(strval($v));
            $len = strlen($value);
            if ($len > 1 && substr($value, 0, 1) == "\"" && substr($value, $len, $len - 1) == "\"")
                $value = substr($value, 1, $len - 1);
            $value = trim($value);
            if ($value == "" || $value == "null")
                continue;
            array_push($rList, $value);
        }
        array_push($rList, $this->app['config']['salt']);
        sort($rList, SORT_STRING);
        return md5(implode('&', $rList));
    }

    private function checkCallbackSignature($map)
    {
        $signed = $map['msg_signature'];
        unset($map['msg_signature'], $map['type']);
        $map = array_values($map);
        $map[] = $this->app['config']['token'];
        sort($map, SORT_STRING);
        return $signed === sha1(implode('', $map));
    }
}
